5.3 Ereignisse eines Objekts  
Die meisten Objekte, mit denen wir es täglich zu tun haben, reagieren auf Anstöße von außen: Ein Auto kann hupen und fahren, eine Person gehen und sprechen. Äußere Anstöße können – projiziert auf den Programmentwurf – Methodenaufrufen gleichgesetzt werden. Ein Client verwaltet beispielsweise ein Objekt vom Typ Car und ruft die Methode Fahren auf. Daraufhin setzt sich das Auto in Bewegung – zumindest aus dem programmtechnischen Blickwinkel heraus.
Die Erfahrung des täglichen Lebens zeigt aber auch, dass Objekte auf diese Anstöße ihrerseits selbst reagieren können. Stellen Sie sich vor, Sie würden in einem Mietshaus wohnen und die Lautstärke der Stereoanlage zu hoch drehen. Wenn Sie Glück haben, werden Ihre Nachbarn das stillschweigend akzeptieren, möglicherweise werden Sie aber auch noch soeben das Klingeln an der Wohnungstür vernehmen und einem vielleicht freundlich, vielleicht auch verärgert um mehr Ruhe bettelnden Nachbarn entgegentreten.
Ein Methodenaufruf ist der Anstoß eines Clients an ein Objekt, damit dieses eine bestimmte Verhaltensweise zeigt. Die Konsequenz eines Methodenaufrufs könnte sein, dass das Objekt seinerseits beim Aufrufer eine Reaktion auslöst. Diese Reaktion lässt sich ebenfalls programmiertechnisch erfassen: Sie wird als Ereignis oder auch – mit dem englischen Begriff – als Event bezeichnet. Ereignisse spielen eine herausragende Rolle bei der Programmierung grafischer Benutzeroberflächen. Sie lassen sich so abstrahieren, dass sie als Nachrichtenverkehr zwischen einer Ereignisquelle und einem Ereignisempfänger angesehen werden können. Eine Ereignisquelle könnte beispielsweise die Schaltfläche in einem Windows-Fenster sein. Sobald der Anwender mit der Maus auf die Schaltfläche klickt, wird ein Ereignis ausgelöst, auf das der Ereignisempfänger reagieren kann, aber nicht muss.
Die Richtung eines Methodenaufrufs geht immer vom Aufrufer zum Objekt. Das Objekt führt danach die Methode aus. Die Richtung eines Ereignisses ist genau entgegengesetzt: Sie geht vom Objekt zurück zum Aufrufer und hier zu einer Methode, dem Ereignisempfänger. Unter diesem Blickwinkel betrachtet ruft ein Objekt als Ereignisquelle eine ihm bekannte Methode im Client, dem Ereignisempfänger, auf.
Der Zeitpunkt der Ereignisauslösung ist im Code eines Objekts festgelegt. Das Besondere an einem Event ist, dass der Ereignisempfänger auf die Auslösung ganz individuell reagieren kann, sie unter Umständen sogar einfach ignoriert.
5.3.1 Ergänzung eines Ereignisses in einer Ereignisquelle  
Die theoretische Betrachtung eines Ereignisses soll nun an einem praktischen Beispiel gezeigt werden. Erinnern wir uns dazu zunächst an die aktuelle Implementierung der Eigenschaftsmethode Radius in der Circle-Klasse:
| Public Property Radius() As Double
|
| Get
|
| Return dblRadius
|
| End Get
|
| Set(ByVal Value As Double)
|
| If Value >= 0 Then
|
| dblRadius = Value
|
| Else
|
| Console.WriteLine("Unzulässiger Wert.")
|
| End If
|
| End Set
|
| End Property
|
Uns interessiert insbesondere der Set-Accessor und dort wiederum das Verhalten der Methode, wenn der Eigenschaft ein negativer Wert übergeben wird. Nach dem derzeitigen Stand führt das zu einer Ausgabe an der Konsole.
Die Implementierung funktioniert tadellos, unterliegt jedoch einer Einschränkung: Der Client muss die Nachricht entgegennehmen – ob er will oder nicht. Anstatt an der Konsole immer nur dieselbe, gleich lautende Meldung anzuzeigen, könnte das Circle-Objekt im Client eine Methode aufrufen. Das Objekt wird in diesem Moment aktiv, denn es löst ein Ereignis aus. Der ursprüngliche Methodenaufrufer kann als Ereignisempfänger auf das Ereignis reagieren, indem in ihm eine bestimmte Methode mit einem vom Client gewünschten Verhalten ausgeführt wird.
Bisher hatten wir es immer mit der Aufrufrichtung ausgehend von einem Client zu einem Objekt zu tun. Ein Ereignis dreht diese Richtung um. Um die Wirkungsweise der Ereignisse besser zu verstehen, wollen wir nun die Klasse Circle um ein solches Ereignis erweitern und es MeasureError nennen.
Der Programmablauf bis zu einer eventuellen Ereignisauslösung würde wie folgt aussehen:
| 1. |
Der Benutzer, auch als Client bezeichnet, erzeugt ein Objekt der Klasse Circle und weist der Eigenschaft Radius einen unzulässigen Wert zu. |
| |
|
| 2. |
In der Eigenschaftsmethode wird der übergebene Wert geprüft, die Unzulässigkeit festgestellt und das Ereignis MeasureError ausgelöst mit der Folge, dass im Client nach einer Methode gesucht wird, die das Ereignis behandelt. |
| |
|
| 3. |
Erklärt sich der Client bereit, das Ereignis zu behandeln, wird im Client die dem Ereignis zugeordnete Methode ausgeführt. |
| |
|
Kommen wir nun zu den Details der Ereignisimplementierung in der Ereignisquelle. Jedes Ereignis muss in der Klassendefinition bekannt gegeben werden. Die allgemeine Syntax einer Ereignisdefinition lautet wie folgt:
| ' Syntax der Ereignisdefinition
|
| [Zugriffsmodifizierer] Event Bezeichner As Typ
|
Dem optionalen Zugriffsmodifizierer (der Standard ist Public) folgt die Anweisung Event, dann wird der Bezeichner des Ereignisses angegeben, zum Schluss der Typ des Delegaten. Die Definition unseres Ereignisses erfolgt auf Klassenebene und könnte wie folgt lauten:
| Public Event MeasureError As MeasureErrorEventHandler
|
Diese Variante verlangt natürlich, dass der Delegat bereits definiert ist, beispielsweise mit
| Public Delegate Sub MeasureErrorEventHandler()
|
Sie können auch die folgende Kurzschreibweise benutzen und dann auf die Definition eines Delegaten verzichten:
| Public Event MeasureError()
|
Nun stellt die Klasse Circle den gewünschten Event bereit, und es muss nur noch zum Ausdruck gebracht werden, wann er im Ereignisempfänger ausgelöst wird. Dazu ändern wir die Eigenschaftsmethode Radius wie folgt:
| Public Property Radius() As Double
|
| Get
|
| Return dblRadius
|
| End Get
|
| Set(ByVal Value As Double)
|
| If Value >= 0 Then
|
| dblRadius = Value
|
| Else
|
| RaiseEvent MeasureError()
|
| End If
|
| End Set
|
| End Property
|
Die ursprüngliche Codezeile zur Ausgabe an der Eingabeaufforderung wird durch die Anweisung RaiseEvent unter Angabe des auszulösenden Ereignisses ersetzt. Damit ist die Klassenimplementierung schon vollständig, um der Anforderung zu genügen.
Übergibt der Client der Eigenschaft Radius nun einen Wert, welcher der Bedingung
entspricht, wird die RaiseEvent-Anweisung dazu führen, das Ereignis MeasureError im Benutzer auszulösen.
5.3.2 Die Behandlung eines Ereignisses im Ereignisempfänger  
Damit ein Client ein von einem Objekt ausgelöstes Ereignis empfängt, sind zwei Voraussetzungen im Zusammenhang mit der Deklaration der Objektvariablen zu erfüllen:
| 1. |
Die Referenz auf das Objekt, dessen Ereignisse behandelt werden sollen, muss global deklariert sein und kann daher niemals lokal in einer Methode erfolgen. |
| |
|
| 2. |
In der Deklaration muss das Schlüsselwort WithEvents angegeben werden. |
| |
|
Betrachten Sie das folgende Codefragment, das den genannten Bedingungen genügt.
| Class ClassA
|
| Private WithEvents obj As Circle
|
| Sub Main()
|
| obj = New Circle()
|
| ' Anweisungen
|
| End Sub
|
| End Class
|
Damit ist die notwendige Vorarbeit geleistet. Es muss nun noch der Ereignishandler syntaktisch korrekt definiert werden. Sie können diesen natürlich nach festgelegten Kriterien manuell schreiben, aber der Code-Editor der Entwicklungsumgebung leistet in diesem Punkt wertvolle Unterstützung, die auch in Anspruch genommen werden sollte.
Im oberen Teil des Code-Editors befinden sich zwei Drop-down-Listenfelder (siehe Abbildung 5.1). Das linke listet alle in der Quellcodedatei definierten Klassen, Module und Strukturen auf, im rechten die zu der Auswahl in der linken Liste zugehörigen Methoden. Wird in beiden Listenfeldern jeweils eine bestimmte Auswahl getroffen, springt der Eingabecursor in den Quellcode der gewählten Elementfunktion. Damit ist auch die Aufgabe der Listenfelder klar umrissen: Sie dienen der Navigationsunterstützung.
 Hier klicken, um das Bild zu Vergrößern
Abbildung 5.1 Der Code-Editor nach der Auswahl des klassenspezifischen Ereignisses
Eine mit WithEvents deklarierte Objektvariable wird ebenfalls im linken Listenfeld aufgeführt. Wählen Sie ein solches Element, können Sie im rechten Listenfeld eines der vom Objekt veröffentlichten Ereignisse selektieren. Haben Sie beispielsweise in einer Klasse oder einem Modul ein Objekt vom Typ der Klasse Circle deklariert, wird der Event MeasureError in der rechten Liste angezeigt. Interessant ist die Unterstützung der Entwicklungsumgebung, die nach der Wahl des gewünschten Ereignisses in der rechten Drop-down-Liste automatisch den syntaktisch korrekten Ereignishandler generiert:
| Public Sub kreis_MeasureError() Handles kreis.MeasureError
|
| End Sub
|
Die Definition eines Ereignishandlers mit Sub macht die nahe Verwandtschaft zu einer herkömmlichen Methode ohne Rückgabewert deutlich. Der Bezeichner setzt sich standardmäßig aus dem Namen des konkreten Objekts, das den Event auslöst (hier: kreis), und dem Namen des Ereignisses in der Klassendefinition zusammen. Beide werden durch einen Unterstrich voneinander getrennt. In unserem Beispiel wird die leere Parameterliste durch die runden Klammern angedeutet. Wie Sie bereits wissen, können bei der Auslösung des Events auch Parameter eine Rolle spielen, deshalb wird auch eine Parameterliste erzeugt, die im Beispiel oben leer ist. Hinter der Parameterliste besagt die Handles-Klausel, welches oder welche Ereignisse von der Methode bedient werden. Wir werden auf die Parameterliste und die Handles-Klausel in den folgenden beiden Abschnitten noch näher eingehen
Fassen wir noch einmal zusammen, was zu der Auslösung eines Events führt:
| 1. |
Auf Klassenebene wird eine Objektvariable deklariert. In Kenntnis davon, dass das Objekt in der Lage ist, Events in bestimmten Situationen auszulösen, wird die Objektreferenz mit dem Schlüsselwort WithEvents deklariert. |
| |
|
| 2. |
Bei der Zuweisung eines Wertes an die Eigenschaft Radius überprüft die Eigenschaftsmethode die Zulässigkeit. Unter der Bedingung, dass der Wert kleiner null ist, führt die Anweisung RaiseEvent dazu, eine dem Objekt namentlich bekannte Methode im Client aufzurufen. Dieser Vorgang wird als Ereignisauslösung bezeichnet. |
| |
|
| 3. |
Enthält die Ereignisprozedur im Ereignisempfänger Programmcode, wird dieser ausgeführt und nach der Beendigung die Kontrolle an den Ereignisauslöser zurückgegeben. |
| |
|
Wie sich der Ereignisempfänger verhält, ob er die Ereignisauslösung ignoriert oder darauf individuell reagiert, bleibt ihm selbst überlassen. Angenommen, der Radius des Kreisobjekts soll in diesem Fall auf den benutzerdefinierten Standardwert null gesetzt werden, könnte im Client das Ereignis wie folgt ergänzt werden:
| Public Sub kreis_MeasureError() Handles kreis.MeasureError
|
| kreis.Radius = 0
|
| End Sub
|
Diese Lösung ist sicherlich einfach, lässt aber andererseits dem Anwender auch keine Chance zu einer neuerlichen, dann vielleicht korrekten Wertzuweisung. Diese Einschränkung soll im folgenden Codefragment aufgehoben werden:
| Public Sub kreis_MeasureError() Handles kreis.MeasureError
|
| Console.WriteLine("Unzulässiger negativer Radius.")
|
| Console.Write("Neueingabe: ")
|
| kreis.Radius = Console.ReadLine()
|
| End Sub
|
Der Anwender wird dazu aufgefordert, dem Kreisobjekt einen Radius größer oder gleich Null zuzuweisen. Gibt er erneut einen negativen Wert ein, wird das Ereignis MeasureError noch einmal ausgelöst – und zwar so lange, bis eine zulässige Eingabe erfolgt. Das ähnelt sehr stark einer Endlosschleife, aus der es kein Entkommen mehr gibt, obwohl hier der Weg aus dem Kreislauf durch die Eingabe eines positiven Werts möglich ist. In der Praxis sollte an dieser Stelle wohl auch noch eine weitere Möglichkeit des vorzeitigen Ausstiegs vorgesehen werden.
5.3.3 Ereignisse mit Übergabeparameter  
Der Schlüssel zu einer optimalen Lösung liegt schon in der Definition des Ereignisses in der Klasse: Wir erzwingen die Übergabe eines Parameters.
| Public Event MeasureError(k As Circle)
|
Die Idee, die hinter dem Parameter steckt, ist, dem Ereignisempfänger über diesen Parameter die Referenz auf das ereignisauslösende Objekt zu übergeben. Darüber kann der Radius erneut festgelegt werden.
Die Ergänzung der verbesserten Ereignisdefinition muss nun noch im Set-Accessor berücksichtigt werden:
| Set(ByVal Value As Double)
|
| If Value >= 0 Then
|
| dblRadius = Value
|
| Else
|
| RaiseEvent MeasureError(Me)
|
| End If
|
| End Set
|
Wird die Set-Unterprozedur aufgerufen, erfolgt auch weiterhin zuerst eine Gültigkeitsüberprüfung. Fällt diese negativ aus, weil im impliziten Parameter ein Wert kleiner null enthalten ist, wird das Ereignis MeasureError im Benutzer ausgelöst. Dabei wird dem Ereignisempfänger mit Me die Referenz auf das Objekt mitgeliefert, welches das Ereignis verursacht hat. Im Ereignishandler kann die übergebene Referenz benutzt werden, um der Eigenschaft Radius nun endlich einen gültigen Wert zuzuweisen.
| Module Module1
|
| Dim WithEvents kreis As Circle
|
| Sub Main()
|
| kreis = New Circle()
|
| kreis.Radius = –2
|
| Console.WriteLine(kreis.Radius)
|
| Console.ReadLine()
|
| End Sub
|
| Private Sub kreis_MeasureError(ByVal k As Circle) _
|
| Handles kreis.MeasureError
|
| Console.WriteLine("Radius {0} unzulässig.", k.Radius)
|
| Console.Write("Neueingabe: ")
|
| k.Radius = Console.ReadLine()
|
| End Sub
|
| End Module
|
5.3.4 Die Handles-Klausel  
Sie haben in den letzten Abschnitten gesehen, wie sich der Name eines Ereignishandlers aus dem Bezeichner der Objektvariablen und dem Bezeichner des Ereignisses, getrennt durch einen Unterstrich, zusammensetzt, beispielsweise:
| Public Sub kreis_MeasureError(k As Circle) _
|
| Handles kreis.MeasureError
|
Die sich der Parameterliste anschließende Handles-Anweisung haben wir bisher noch nicht betrachtet. Wir werden uns ihr nun widmen, da sie weitere Möglichkeiten zur Reaktion auf ausgelöste Ereignisse eröffnet.
Für die Behandlung eines Ereignishandlers im Ereignisempfänger ist nicht der Bezeichner einer Methode das alles entscheidende Kriterium, sondern die Angabe hinter der Handles-Anweisung. Sie besagt, welches Ereignis vom Ereignishandler behandelt werden kann, in unserem Beispiel das Ereignis MeasureError des Objekts mit dem Namen kreis. Der Bezeichner kreis_MeasureError, der von der Entwicklungsumgebung vergeben worden ist, kann beliebig geändert werden, weil nur Handles für die eindeutige Zuordnung eines Ereignishandlers zu einem Ereignis verantwortlich ist.
Sie können mit einem Ereignishandler die Events mehrerer Objekte behandeln, indem Sie die Liste hinter der Handles-Anweisung, wie im folgenden Codefragment gezeigt wird, erweitern.
| Public Sub CompleteHandler() Handles kreis1.MeasureError, _
|
| kreis2.MeasureError
|
| ...
|
| End Sub
|
Diese Methode wird ausgeführt, wenn das Ereignis MeasureError entweder von Objekt kreis1 oder Objekt kreis2 ausgelöst wird.
5.3.5 Die Registrierung eines Ereignishandlers mit »AddHandler«  
Es gibt noch einen zweiten Weg, der uns unter Visual Basic 2005 die Anbindung eines Ereignisses an einen Ereignishandler ermöglicht. Ermöglicht wird dies durch die Anweisung AddHandler. Der Code, der sich im Benutzer durch den Einsatz von AddHandler ändert, zeigt das folgende Codefragment.
| Module Module1
|
| Sub Main()
|
| Dim kreis As New Circle
|
| AddHandler kreis.MeasureError, AddressOf kreis_MeasureError
|
| kreis.Radius = –3
|
| Console.ReadLine()
|
| End Sub
|
| Private Sub kreis_MeasureError(ByVal k As Circle)
|
| Console.WriteLine("Radius ist nicht zugelassen.")
|
| Console.Write("Neueingabe: ")
|
| k.Radius = Console.ReadLine()
|
| End Sub
|
| End Module
|
AddHandler bindet das Ereignis eines Objekts an eine bestimmte Methode. Im ersten Argument des Statements werden dabei das Objekt und das Ereignis aufgeführt, dessen Auslösung behandelt werden soll. Im zweiten Argument wird hinter AddressOf die aufzurufende Methode genannt, wenn das im ersten Argument bekannt gegebene Ereignis im Objekt ausgelöst wird. Die allgemeine Syntax lautet:
| AddHandler <Objekt>.<Ereignis>, AddressOf <Objekt>.<Methodenname>
|
Schauen Sie sich den Code oben noch einmal genau an. Interessant ist die Tatsache, dass es nun keine Handles-Klausel mehr gibt, die das Ereignis statisch an die Methode bindet, sondern dass die Bindung nun im Programmcode erfolgt. Zudem taucht im Programmcode keine WithEvents-Anweisung mehr auf.
|
Um ein Ereignis statisch mit der Handles-Klausel an eine Prozedur zu binden, ist die Deklaration einer Objektvariablen mit dem WithEvents-Statement auf Klassenebene Voraussetzung. Solche Bindungen sind statisch. Bei der dynamischen Bindung mit AddHandler kann auf WithEvents verzichtet werden. Zudem darf die Objektvariable innerhalb einer Methode deklariert sein.
|
Die Bindung mit AddHandler ist ausgesprochen flexibel, denn sie kann auch wieder gelöst werden. Dazu dient die Anweisung RemoveHandler, deren Syntax identisch mit der von AddHandler ist.
| RemoveHandler kreis.MeasureError, AddressOf kreis_MeasureError
|
Kommt es nun zu einer Auslösung des Ereignisses MeasureError des Objekts kreis, wird das Ereignis keinen Abnehmer mehr finden.
5.3.6 Zusammenfassung  
|
Die Richtung eines herkömmlichen Methodenaufrufs geht von einem Objektnutzer zum Objekt. Eine Benachrichtigung, die das Objekt an seinen Aufrufer, den Ereignisempfänger, schickt, wird als Ereignis bezeichnet. |
|
Ein Ereignis wird mit dem Schlüsselwort Event definiert. Wird auf die Angabe des Zugriffsmodifizierers verzichtet, gilt dieser standardmäßig als Public. |
|
Mit RaiseEvent unter der Angabe des Bezeichners wird die Anweisung gegeben, im Ereignisempfänger das Ereignis auszulösen. |
|
Damit der Ereignisempfänger in der Lage ist, ein ausgelöstes Ereignis zu behandeln, muss die Objektvariable im Allgemeinteil der Klasse oder des Moduls zusammen mit dem Schlüsselwort WithEvents deklariert sein. |
|
Einem potenziellen Ereignishandler werden durch das Anhängen der Handles-Klausel das Objekt und das Ereignis angegeben, dem der Ereignishandler zugeordnet wird. |
|
Hinter der Handles-Klausel können mehrere Ereignisse getrennt durch ein Komma aufgelistet werden. Die Methode wird danach immer dann ausgeführt, sobald eines der angegebenen Ereignisse ausgelöst wird. |
|
Mit AddHandler können Ereignisse dynamisch an eine Methode gebunden werden. Die Objektvariable darf dabei lokal deklariert sein. Eine mit AddHandler eingeleitete Bindung kann mit RemoveHandler wieder aufgehoben werden. |
|